package com.huawei.bitfun.insight.processor;

import com.alibaba.fastjson.JSONObject;
import com.huawei.bitfun.insight.InsightExContext;
import com.huawei.bitfun.insight.database.ConnectionManager;
import com.huawei.bitfun.insight.database.DatabaseManager;
import com.huawei.bitfun.insight.database.QueryDataType;
import com.huawei.bitfun.insight.processor.message.QueryFromDbRequest;
import com.huawei.bitfun.insight.type.converter.MainQueryConverter;
import com.huawei.bitfun.insight.utils.Common;
import com.huawei.bitfun.insight.utils.DialogManager;
import com.huawei.deveco.insight.ohos.ability.arkcpu.entity.CaptureNode;
import com.huawei.deveco.insight.ohos.common.ProfilerError;
import com.huawei.deveco.insight.ohos.common.ProfilerException;
import com.huawei.deveco.insight.ohos.common.Response;
import com.huawei.deveco.insight.ohos.common.enums.CallStackFrameCategory;
import com.huawei.deveco.insight.ohos.model.vo.HiPerfNode;
import com.huawei.deveco.insight.ohos.processor.HiPerfProcessor;
import com.huawei.deveco.insight.ohos.resourcehandler.controller.RequestMapping;
import com.huawei.deveco.insight.ohos.service.HiPerfService;
import com.huawei.deveco.insight.ohos.service.ark.ArkCpuService;
import com.huawei.deveco.insight.ohos.service.ark.ArkRecordManager;
import com.huawei.deveco.insight.ohos.utils.BalloonNotification;
import com.huawei.deveco.insight.ohos.utils.DocumentUtils;
import com.huawei.deveco.insight.ohos.utils.JsonUtil;
import com.huawei.deveco.insight.ohos.utils.StringUtil;
import com.huawei.deveco.insight.ohos.utils.ValidateUtil;
import com.intellij.notification.NotificationType;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.fileChooser.FileSaverDescriptor;
import com.intellij.openapi.fileChooser.FileSaverDialog;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.progress.Task;
import com.intellij.openapi.progress.impl.BackgroundableProcessIndicator;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.openapi.vfs.VirtualFileWrapper;
import org.apache.commons.dbcp2.BasicDataSource;
import org.apache.commons.io.FilenameUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.awt.Desktop;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;

import static com.huawei.bitfun.insight.utils.Common.ARK_CPU_SOURCE_DATA_FILE_EXTENSION;
import static com.huawei.bitfun.insight.utils.Common.HIPERF_SOURCE_DATA_FILE_EXTENSION;
import static com.huawei.bitfun.insight.utils.Common.MANIFEST_FILE_NAME;

@RequestMapping(path = "main.query")
public class MainQueryProcessor {
    private static final Logger LOGGER = LoggerFactory.getLogger(MainQueryProcessor.class);

    public static List<Map<String, Object>> queryList;

    private static final String CSV_FILE_EXTENSION = ".csv";

    private static List<String> sessionList = new ArrayList<>();

    @RequestMapping(path = "query")
    public static Response<?> query(JSONObject params) {
        try {
            QueryFromDbRequest request = JsonUtil.parseObject(params, QueryFromDbRequest.class);
            ValidateUtil.validate(Objects.requireNonNull(request));
            BasicDataSource dataSource = ConnectionManager.getDataSource(request.getDataSourceId(),
                    request.getDataSourcePath());
            QueryDataType<Map<String, Object>> queryDataType =
                    DatabaseManager.queryData(request.getSql(), dataSource, new MainQueryConverter());
            queryList = queryDataType.getDataList();
            return Response.success(queryDataType);
        } catch (ProfilerException e) {
            BalloonNotification.show("sql error: " + e.getMessage(), NotificationType.WARNING);
            return Response.failure(e.getInsightError());
        }
    }

    @RequestMapping(path = "saveAsCsv")
    public static Response<?> saveAsCsv(JSONObject params) {
        if (queryList == null || queryList.isEmpty()) {
            return Response.failure(ProfilerError.EXPORT_ERROR);
        }
        File file = showDialogInThreadUI();
        boolean isFileValid = checkFileValid(file);
        if (!isFileValid) {
            return Response.failure(ProfilerError.EXPORT_ERROR);
        }
        Task.Backgroundable task = new Task.Backgroundable(InsightExContext.getProject(), "Save as csv", false) {
            @Override
            public void run(@NotNull ProgressIndicator progressIndicator) {
                try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) {
                    writer.write(String.join(",", queryList.get(0).keySet()));
                    writer.newLine();
                    for (Map<String, Object> map : queryList) {
                        String rowStr = map.values().stream().map((v) -> {
                            if (v == null) {
                                return "";
                            } else {
                                return v.toString();
                            }
                        }).collect(Collectors.joining(","));
                        writer.write(rowStr);
                        writer.newLine();
                    }
                    Desktop.getDesktop().open(file.getParentFile());
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        };
        BackgroundableProcessIndicator indicator = new BackgroundableProcessIndicator(task);
        ProgressManager.getInstance().runProcessWithProgressAsynchronously(task, indicator);
        return Response.success();
    }

    @RequestMapping(path = "functionQuery")
    public static Response<?> functionQuery(JSONObject params) {
        QueryFromDbRequest request = JsonUtil.parseObject(params, QueryFromDbRequest.class);
        ValidateUtil.validate(Objects.requireNonNull(request));
        String path = FilenameUtils.getFullPath(request.getDataSourcePath());
        // 解析manifest文件
        String manifestStr = DocumentUtils.readFileToString(Common.joinWithForwardSlash(path, MANIFEST_FILE_NAME));
        JSONObject manifestJson = JSONObject.parseObject(manifestStr);
        String sessionId = manifestJson.getString("id");
        int pid = manifestJson.getJSONObject("process").getInteger("pid");
        String deviceKey = manifestJson.getJSONObject("device").getString("deviceKey");
        if (!sessionList.contains(sessionId)) {
            if (!createArkCpuService(sessionId, path) || !createHiPerfService(sessionId, path, pid, deviceKey)) {
                ArkRecordManager.getInstance().releaseArkCpuProfilerService(sessionId);
                HiPerfProcessor.getInstance().releaseHiPerfService(sessionId);
                return Response.failure(ProfilerError.FILE_PARSING_ERROR);
            }
            sessionList.add(sessionId);
        }
        // 获取ark cpu service
        Optional<ArkCpuService> arkCpuServiceOptional = ArkRecordManager.getInstance().getArkCpuService(sessionId);
        if (arkCpuServiceOptional.isEmpty()) {
            return Response.failure(ProfilerError.FILE_PARSING_ERROR);
        }
        List<Map<String, Object>> resultList
                = new ArrayList<>(searchFunctionInArkCpu(arkCpuServiceOptional.get(), request.getSql()));
        // 获取hiperf service
        Optional<HiPerfService> hiPerfServiceOptional = HiPerfProcessor.getInstance().getHiPerfService(sessionId);
        if (hiPerfServiceOptional.isEmpty()) {
            return Response.failure(ProfilerError.FILE_PARSING_ERROR);
        }
        resultList.addAll(searchFunctionInHiPerf(hiPerfServiceOptional.get(), request.getSql()));
        queryList = resultList;
        QueryDataType<Map<String, Object>> queryDataType = new QueryDataType<>();
        queryDataType.setDataList(resultList);
        queryDataType.setColumnList(Arrays.asList("FunctionName", "ThreadId", "StartTime", "Duration", "Source", "Type"));
        return Response.success(queryDataType);
    }

    private static boolean createHiPerfService(String sessionId, String path, int pid, String deviceKey) {
        Optional<File> hiperfDataFileOption = Arrays.stream(new File(path).listFiles()).filter((file ->
                FilenameUtils.isExtension(file.getName(), HIPERF_SOURCE_DATA_FILE_EXTENSION))).findAny();
        if (hiperfDataFileOption.isEmpty()) {
            LOGGER.warn("Can't find hiperf data file in unzip path: {}", path);
            return false;
        }
        File hiperfDataFile = hiperfDataFileOption.get();
        // hiperf解析
        if (!HiPerfProcessor.getInstance().handleHiperfData(deviceKey, pid,
                DocumentUtils.getFileCanonicalPath(hiperfDataFile), sessionId)) {
            return false;
        }
        return true;
    }

    private static List<Map<String, Object>> searchFunctionInHiPerf(HiPerfService service, String keyword) {
        List<Map<String, Object>> list = new ArrayList<>();
        List<Integer> tidList = service.getMetadata().getTid();

        for (Integer tid : tidList) {
            HiPerfNode node = service.getHiPerfNodes().get(tid);
            if (node != null) {
                list.addAll(searchFunctionInHiPerfDfs(node, keyword, tid));
            }
        }
        return list;
    }
    private static List<Map<String, Object>> searchFunctionInHiPerfDfs(HiPerfNode node, String functionName, int tid) {
        List<Map<String, Object>> list = new ArrayList<>();
        if (isHiPerfFunctionNameMatchWithNoCase(node, functionName)) {
            for (int i = 0; i < node.getStartBootTimeList().size(); i++) {
                list.add(createFunctionMap(functionName, tid, node.getStartBootTimeList().get(i)/1000,
                        (node.getEndBootTimeList().get(i) - node.getStartBootTimeList().get(i))/1000, node.getSource(), "Callstack"));
            }
        }
        if (node.getChildren() == null) {
            return list;
        }
        for (HiPerfNode child : node.getChildren()) {
            list.addAll(searchFunctionInHiPerfDfs(child, functionName, tid));
        }
        return list;
    }


    private static boolean createArkCpuService(String sessionId, String path) {
        ArkCpuService arkCpuService = ArkRecordManager.getInstance().creatArkCpuService(sessionId);
        List<File> akrCpuFileList = Arrays.stream(new File(path).listFiles()).filter((file ->
                FilenameUtils.isExtension(file.getName(), ARK_CPU_SOURCE_DATA_FILE_EXTENSION))).toList();
        for (File akrCpuFile : akrCpuFileList) {
            String filePath = DocumentUtils.getFileCanonicalPath(akrCpuFile);
            int tid = StringUtil.getFileTidFromPath(filePath);
            if (!arkCpuService.readAndParseCpuProfile(sessionId, tid, filePath, 0)) {
                LOGGER.warn("Failed to read cpu profiler file. path: {}", filePath);
                return false;
            }
        }
        return true;
    }

    private static List<Map<String, Object>> searchFunctionInArkCpu(ArkCpuService service, String functionName) {
        List<Map<String, Object>> list = new ArrayList<>();
        Class<?> ArkCpuServiceClass = ArkCpuService.class;
        try {
            Field mapField = ArkCpuServiceClass.getDeclaredField("captureTreeMap");
            mapField.setAccessible(true);
            Map<Integer, CaptureNode> captureTreeMapCopy = (Map<Integer, CaptureNode>) mapField.get(service);

            for (Map.Entry<Integer, CaptureNode> entry: captureTreeMapCopy.entrySet()) {
                list.addAll(searchFunctionInArkCpuDfs(entry.getValue(), functionName, entry.getKey()));
            }
            return list;
        } catch (NoSuchFieldException | IllegalAccessException e) {
            return new ArrayList<>();
        }
    }

    private static List<Map<String, Object>> searchFunctionInArkCpuDfs(CaptureNode node, String functionName, int tid) {
        List<Map<String, Object>> list = new ArrayList<>();
        node.getData().getName();
        if (isArkCpuFunctionNameMatchWithNoCase(node,functionName)) {
            list.add(createFunctionMap(functionName, tid, node.getStartTime(),
                    node.getEndTime() - node.getStartTime(), node.getData().getUrl(), "ArkTS Callstack"));
        }
        for (CaptureNode child : node.getChildren()) {
            list.addAll(searchFunctionInArkCpuDfs(child, functionName, tid));
        }
        return list;
    }

    private static boolean isHiPerfFunctionNameMatchWithNoCase(HiPerfNode node, String keyword) {
        return node.getFunctionName() != null
                && node.getFunctionName().toLowerCase(Locale.ENGLISH).contains(keyword.toLowerCase(Locale.ENGLISH));
    }

    private static boolean isArkCpuFunctionNameMatchWithNoCase(CaptureNode node, String keyword) {
        if (node.getFrameCategory().name().toLowerCase(Locale.ENGLISH).equals(CallStackFrameCategory.NAPI.name().toLowerCase(Locale.ENGLISH))) {
            String sourceFunctionName = node.getData().getName().replaceAll("\\(.*?\\)", "").toLowerCase(Locale.ENGLISH);
            return sourceFunctionName.contains(keyword.toLowerCase(Locale.ENGLISH));
        } else {
            return node.getData().getName().toLowerCase(Locale.ENGLISH).contains(keyword.toLowerCase(Locale.ENGLISH));
        }
    }

    private static Map<String, Object> createFunctionMap(
            String functionName, int tid, long startTime, long duration, String source,String type) {
        Map<String, Object> map = new HashMap<>();
        map.put("FunctionName", functionName);
        map.put("ThreadId", tid);
        map.put("StartTime", startTime);
        map.put("Duration", duration);
        map.put("Source", source);
        map.put("Type", type);
        return map;
    }

    private static boolean checkFileValid(File file) {
        if (file == null) {
            LOGGER.info("User Cancel exporting csv file.");
            return false;
        }
        if (file.exists() && !file.delete()) {
            LOGGER.info("Delete file error.");
            return false;
        }
        return true;
    }

    @Nullable
    private static File showDialogInThreadUI() {
        AtomicReference<File> file = new AtomicReference<>();
        ApplicationManager.getApplication().invokeAndWait(() -> {
            FileSaverDescriptor saverDescriptor = new FileSaverDescriptor("Export",
                    "Please select a valid folder", "csv");
            FileSaverDialog saverDialog = DialogManager.getSaveFileDialog(saverDescriptor);

            String fileName = "profilerEx" + Common.getLocalTime();
            if (SystemInfo.isMac || SystemInfo.isLinux) {
                fileName = fileName + CSV_FILE_EXTENSION;
            }
            VirtualFileWrapper fileWrapper = saverDialog.save(VfsUtil.getUserHomeDir(), fileName);
            if (fileWrapper != null) {
                File saveFile = fileWrapper.getFile();
                if (!fileName.toLowerCase(Locale.ENGLISH).endsWith(CSV_FILE_EXTENSION)) {
                    File newFile = new File(saveFile.getParent(), fileName + CSV_FILE_EXTENSION);
                    file.set(newFile);
                } else {
                    file.set(saveFile);
                }
            } else {
                file.set(null);
            }
        });
        return file.get();
    }
}
